package othello.util;

import java.text.MessageFormat;
import java.util.logging.Logger;

import org.slim3.memcache.Memcache;

/**
 * <p>
 * GAE上でのSynchronizedブロックを実現するクラス
 * </p>
 * Memcacheでのスピンロック実装により実現する。
 * 
 * @author onjiro.mohyahya
 */
public class SynchronizedRunner {

	/**
	 * ロック実行の最大回数
	 */
	private static final int LOCK_TRY_MAX_COUNT = 5;

	/**
	 * ロックを再試行する間隔
	 */
	private static final long LOCK_TRY_INTERVAL = 100L;

	/**
	 * ロックを識別するキー
	 */
	private Object lockKey;

	/**
	 * ロックを識別するキーを指定するコンストラクター
	 * 
	 * @param lockKey
	 *            ロックを識別するキー
	 */
	public SynchronizedRunner(Object lockKey) {
		this.lockKey = lockKey;
	}

	/**
	 * 指定したRunnableのrunメソッド部分を同期実行する。
	 * 
	 * @param lockKey
	 *            ロックを識別するキー
	 * @param synchronizedParts
	 *            同期して実行する動作
	 * @throws LockFailed
	 *             ロックに失敗した場合
	 */
	public void run(final java.lang.Runnable... synchronizedParts) {
		// ロック処理自体は引数ありの run に移譲する
		this.run(new Runnable<Void>() {
			@Override
			public Void run() {
				for (java.lang.Runnable part : synchronizedParts) {
					part.run();
				}
				return null;
			}
		});
	}

	/**
	 * 指定したRunnableのrunメソッド部分を同期実行する。
	 * 
	 * @param lockKey
	 *            ロックを識別するキー
	 * @param synchronizedParts
	 *            同期して実行する動作
	 * @throws LockFailed
	 *             ロックに失敗した場合
	 */
	public <T> T run(Runnable<T> part) {

		// ロックを取得
		if (tryLock(this.lockKey, LOCK_TRY_MAX_COUNT, LOCK_TRY_INTERVAL)) {
			try {
				// 同期指定部分を実行する
				return part.run();

			} finally {
				// ロックを開放する
				releaseLock(this.lockKey);
			}
		} else {
			// ロック失敗時は例外発生
			String message = MessageFormat.format(
					"Syncメソッド用のロックの取得に失敗。lockKey=\"{0}\"", this.lockKey);
			Logger.getLogger(this.getClass().getName()).warning(message);
			throw new LockFailed();
		}
	}

	/**
	 * 指定回数までロックの取得を試行
	 * 
	 * @param lockKey
	 *            ロックに使用するキー
	 * @param maxTryCount
	 *            ロックの試行回数
	 * @return true:ロック成功, false:ロック失敗
	 */
	private boolean tryLock(Object lockKey, int maxTryCount, long interval) {
		boolean locked = false;
		int tryCount = 0;

		while ((locked = acquireLock(lockKey)) == false
				|| tryCount >= maxTryCount) {

			tryCount++;
			try {
				Thread.sleep(interval);
			} catch (InterruptedException e) {
				// 割り込み例外は無視する
			}
		}
		return locked;
	}

	/**
	 * 指定されたキーに対するロックを取得します。
	 * 
	 * @param lockKey
	 *            ロックを識別するキー
	 * @return true:ロック成功 false:ロック失敗
	 */
	private boolean acquireLock(Object lockKey) {
		Long lockValue = Memcache.increment(lockKey, 1L, 0L);
		return (lockValue.longValue() == 1L) ? true : false;
	}

	/**
	 * 指定されたキーに対するロックを開放します。
	 * 
	 * @param lockKey
	 *            ロックを識別するキー
	 */
	private void releaseLock(Object lockKey) {
		Memcache.increment(lockKey, -1L);
	}

	/**
	 * エンクロージングクラスでのロック失敗時の例外クラス
	 * 
	 * @author onjiro.mohyahya
	 * 
	 */
	@SuppressWarnings("serial")
	public static final class LockFailed extends RuntimeException {
	}

	/**
	 * 同期実行で実行されるコード部分のインターフェース
	 * 
	 * @param <T>
	 *            実行結果の戻り値
	 * @author onjiro.mohyahya
	 */
	public static interface Runnable<T> {
		public T run();
	}
}
